//========================================
// This miniplug has a standalone version available in the plugin market.
// https://www.npmjs.com/package/koishi-plugin-microcommands
//========================================

import { Command, SessionError, h, union } from "koishi"

export const name = "microcommands"
export const inject = ["database"]

export function apply(ctx) {
  ctx.model.extend("microcommand", {
    name: "string",
    code: "text",
  }, {
    primary: "name",
  })

  const cmd = ctx.command("microcommand")

  const forks = Object.create(null)
  function load(name, code) {
    unload(name)
    new Function(code)
    forks[name] = ctx.plugin(ctx => {
      let inject = []
      let signature = ""
      let properties = []
      let options = []
      let aliases = []
      let usage = ""
      let examples = []
      let userFields = []
      let channelFields = []
      let checkers = []
      let actions = []
      const vars = {
        name,
        ctx,
        require,
        ...require("koishi"),
        inject(v) {
          inject = v
        },
        signature(sig, ...rest) {
          if (typeof sig !== "string") throw new TypeError("usage must be a string")
          signature = sig
          properties = rest
        },
        option(...args) {
          options.push(args)
        },
        alias(...args) {
          aliases.push(args)
        },
        usage(v) {
          if (typeof v !== "string" && typeof v !== "function") throw new TypeError("usage must be a string or a function")
          usage = v
        },
        example(v) {
          if (typeof v !== "string") throw new TypeError("usage must be a string")
          examples.push(v)
        },
        userFields(v) {
          userFields = union(userFields, v)
        },
        channelFields(v) {
          channelFields = union(channelFields, v)
        },
        before(v, append) {
          if (typeof v !== "function") throw new TypeError("checker must be a function")
          if (append) checkers.push(v)
          else checkers.unshift(v)
        },
        action(v, prepend) {
          if (typeof v !== "function") throw new TypeError("action must be a function")
          if (prepend) actions.unshift(v)
          else actions.push(v)
        },
        locale(id, value) {
          ctx.i18n.define(id, "commands." + name.replace(/.*\//, ""), value)
        },
      }
      const { ctx: _ctx, setCtx } = (0, eval)(/*js*/ `
        (function ({ ${Object.keys(vars).join(", ")} }) {
          ____;
          return {
            ctx,
            setCtx(v) {
              ctx = v
            },
          }
        })
      `.replace(/\s+/g, " ").replace(/\B[ ]|[ ]\B/g, "").replace("____", code))(vars)
      _ctx.plugin({
        name: "microcommands:" + name,
        inject,
        apply(ctx) {
          setCtx(ctx)
          const fullName = name.includes(".") ? name : `microcommand/${name}`
          const subCmd = ctx.command(`${fullName} ${signature}`, ...properties)
          for (const o of options) subCmd.option(...o)
          for (const a of aliases) subCmd.alias(...a)
          if (usage) subCmd.usage(usage)
          for (const e of examples) subCmd.example(e)
          if (userFields.length) subCmd.userFields(userFields)
          if (channelFields.length) subCmd.channelFields(channelFields)
          for (const c of checkers) subCmd.before(c, true)
          for (const a of actions) subCmd.action(a)
        },
      })
    })
  }
  function unload(name) {
    forks[name]?.dispose()
    delete forks[name]
  }

  const logger = ctx.logger(name)
  ctx.on("ready", async () => {
    const d = await ctx.database.get("microcommand", {}, { sort: { name: "asc" } })
    let successCount = 0
    for (const { name, code } of d) {
      try {
        load(name, code)
        successCount++
      } catch (err) {
        logger.error("load %o", name, err)
      }
    }
    logger.info("successfully loaded %d of %d commands", successCount, d.length)
  })

  cmd.subcommand(".define <name:string> <code:rawtext>", { authority: 4, strictOptions: true })
    .action(async ({ session }, name, code) => {
      if (name == null || code == null) return session.i18n("internal.insufficient-arguments")
      if (!name || name.match(/[\s\x00-\x1f"#%&'()/<>?\x7f-\x9f]|^\.|\.$/)) throw new SessionError(".invalid-name")
      name = Command.normalize(name)
      try {
        load(name, code)
      } catch (err) {
        return h.text(String(err))
      }
      await ctx.database.upsert("microcommand", [{ name, code }])
      return session.i18n(".saved", [name])
    })
  cmd.subcommand(".list")
    .action(async ({ session }) => {
      const l = Object.keys(forks)
      if (!l.length) return session.i18n(".none")
      return [...session.i18n(".header"), ...l.map(n => h("p", n.startsWith("-") ? `"${n}"` : n))]
    })
  cmd.subcommand(".show <name:string>", { strictOptions: true })
    .action(async ({ session }, name) => {
      if (name == null) return session.i18n("internal.insufficient-arguments")
      name = Command.normalize(name)
      const [o] = await ctx.database.get("microcommand", { name })
      if (!o) throw new SessionError(".not-found", [name])
      return h.text(o.code)
    })
  cmd.subcommand(".remove <name:string>", { authority: 4, strictOptions: true })
    .action(async ({ session }, name) => {
      name = Command.normalize(name)
      unload(name)
      await ctx.database.remove("microcommand", { name })
      return session.i18n(".deleted", [name])
    })
  ctx.i18n.define("zh-CN", "commands.microcommand", {
    description: "微指令…",
    define: {
      description: "定义微指令",
      messages: {
        "invalid-name": "指令名无效…",
        saved: "已保存微指令 {0}。",
      },
    },
    list: {
      description: "显示当前注册的微指令列表",
      messages: {
        none: "当前未注册任何微指令。",
        header: "当前注册的微指令有：",
      },
    },
    show: {
      description: "查询指定微指令的源代码",
      messages: {
        "not-found": "微指令 {0} 不存在…",
      },
    },
    remove: {
      description: "删除微指令",
      messages: {
        deleted: "已删除微指令 {0}。",
      },
    },
  })
}
